using System;
using System.Reflection;
using System.Globalization;
using System.Collections.Generic;

namespace Measurements
{
    /// <summary>
    /// A measurement value.
    /// </summary>
    /// <typeparam name="TUnit">
    /// The unit that this value measures.
    /// </typeparam>
    public class Measurement<TUnit> : IComparable, IComparable<Measurement<TUnit>>, IEquatable<Measurement<TUnit>>
        where TUnit : MeasurementUnit
    {
        private readonly IThreadSafeDouble _BaseUnitValue = new LockedThreadSafeDouble();
          
        /// <summary>
        /// Gets or sets the value of the measurement in its base unit.
        /// </summary>
        public double BaseUnitValue
        {
            get { return _BaseUnitValue.Value; }
            set { _BaseUnitValue.Value = value; }
        }
          
        /// <summary>
        /// Initializes a new instance of the <see cref="Measurements.Measurement`1"/> class.
        /// </summary>
        public Measurement() { }
              
        /// <summary>
        /// Initializes a new instance of the <see cref="Measurements.Measurement`1"/> class.
        /// </summary>
        /// <param name='baseUnitValue'>
        /// The initial value of the measurement in its base unit.
        /// </param>
        public Measurement(double baseUnitValue) : this() { BaseUnitValue = baseUnitValue; }
        
        /// <summary>
        /// Gets or sets the value of the measurement  in the specified unit.
        /// </summary>
        /// <param name='unit'>
        /// The unit of the value that is being set or gotten.
        /// </param>
        /// <exception cref='ArgumentNullException'>
        /// Thrown when <paramref name="unit"/> is null.
        /// </exception>
        /// <exception cref='ArgumentException'>
        /// Thrown when the required delegate of <paramref name="unit"/> used for unit conversion is null.
        /// </exception>
        public double this[TUnit unit]
        {
            get 
            { 
                if (unit == null) throw new ArgumentNullException("unit");
                if (unit.FromBaseUnit == null) throw new ArgumentException("Delegate is null.");
                
                return unit.FromBaseUnit(BaseUnitValue); 
            }
            set 
            {
                if (unit == null) throw new ArgumentNullException("unit");
                if (unit.ToBaseUnit == null) throw new ArgumentException("Delegate is null.");
                
                BaseUnitValue = unit.ToBaseUnit(value);
            }
        }
        
        /// <summary>
        /// Compares a measurement to this measurement and returns a value indicating 
        /// whether this measurement precedes, follows, or occurs in the same position
        /// of the sort order as <paramref name="other"/>.
        /// </summary>
        /// <returns>
        /// Less than 0 if this measurement precedes <paramref name="other" />. Greater than 0 if this measurement
        /// follows <paramref name="other"/>. 0 if this measurement occurs in the same position as <paramref name="other"/>.
        /// </returns>
        /// <param name='other'>
        /// The measurement to be compared to this instance.
        /// </param>
        public int CompareTo(Measurement<TUnit> other)
        {
            if (other == null) return 1;
            return BaseUnitValue.CompareTo(other.BaseUnitValue);
        }
  
        /// <summary>
        /// Compares a measurement to this measurement and returns a value indicating 
        /// whether this measurement precedes, follows, or occurs in the same position
        /// of the sort order as <paramref name="obj"/>.
        /// </summary>
        /// <returns>
        /// Less than 0 if this measurement precedes <paramref name="obj" />. Greater than 0 if this measurement
        /// follows <paramref name="obj"/>. 0 if this measurement occurs in the same position as <paramref name="obj"/>.
        /// </returns>
        /// <param name='other'>
        /// The measurement to be compared to this instance.
        /// </param>
        /// <exception cref='ArgumentException'>
        /// Thrown if <paramref name="obj"/> is not an instance of <see cref="Measurements.Measurement`1"/>.
        /// </exception>
        public int CompareTo(object obj)
        {
            // TODO: Should the null check come after the type check?

            if (obj == null) return 1;
            if (obj.GetType() != GetType()) throw new ArgumentException();

            return CompareTo((Measurement<TUnit>)obj);
        }
        
        /// <summary>
        /// Determines whether the specified <see cref="Measurement<TUnit>"/> is equal to the current <see cref="Measurements.Measurement`1"/>.
        /// </summary>
        /// <param name='other'>
        /// The <see cref="Measurement<TUnit>"/> to compare with the current <see cref="Measurements.Measurement`1"/>.
        /// </param>
        /// <returns>
        /// <c>true</c> if the specified <see cref="Measurement<TUnit>"/> is equal to the current
        /// <see cref="Measurements.Measurement`1"/>; otherwise, <c>false</c>.
        /// </returns>
        public bool Equals(Measurement<TUnit> other)
        {
            if (other == null) return false;
            return BaseUnitValue == other.BaseUnitValue;
        }
        
        /// <summary>
        /// Determines whether the specified <see cref="System.Object"/> is equal to the current <see cref="Measurements.Measurement`1"/>.
        /// </summary>
        /// <param name='obj'>
        /// The <see cref="System.Object"/> to compare with the current <see cref="Measurements.Measurement`1"/>.
        /// </param>
        /// <returns>
        /// <c>true</c> if the specified <see cref="System.Object"/> is equal to the current
        /// <see cref="Measurements.Measurement`1"/>; otherwise, <c>false</c>.
        /// </returns>
        public override bool Equals(object obj)
        {
            if (obj == null) return false;
            if (obj.GetType() != GetType()) return false;

            return Equals((Measurement<TUnit>)obj);
        }
        
        /// <summary>
        /// Determines whether the specified <see cref="Measurement<TUnit>"/> is equal to the current <see cref="Measurements.Measurement`1"/>
        /// within the specified <paramref name="tolerance"/>.
        /// </summary>
        /// <param name="other">
        /// The <see cref="Measurement<TUnit>"/> to compare with the current <see cref="Measurements.Measurement`1"/>.
        /// </param>
        /// <param name="tolerance">
        /// The tolerance within which the two measurements are considered equal.
        /// </param>
        /// <returns>
        /// <c>true</c> if the specified <see cref="Measurement<TUnit>"/> is equal to the current
        /// <see cref="Measurements.Measurement`1"/> within the given <paramref name="tolerance"/>; otherwise, <c>false</c>.
        /// </returns>
        /// <remarks>
        /// The base value of the measurement is used for determining the magnitude of <paramref name="tolerance"/>.
        /// </remarks>
        public bool Equals(Measurement<TUnit> other, double tolerance)
        {
            if (other == null) return false;
            return Math.Abs(BaseUnitValue - other.BaseUnitValue) <= tolerance;
        }        
        
        /// <summary>
        /// Determines whether the specified <see cref="Measurement<TUnit>"/> is equal to the current <see cref="Measurements.Measurement`1"/>
        /// within the specified <paramref name="tolerance"/>.
        /// </summary>
        /// <param name='other'>
        /// The <see cref="Measurement<TUnit>"/> to compare with the current <see cref="Measurements.Measurement`1"/>.
        /// </param>
        /// <param name="tolerance">
        /// The tolerance within which the two measurements are considered equal.
        /// </param>
        /// <param name='unit'>
        /// The unit that determines the magnitude of the specified <paramref name="tolerance"/>.
        /// </param>
        /// <returns>
        /// <c>true</c> if the specified <see cref="Measurement<TUnit>"/> is equal to the current
        /// <see cref="Measurements.Measurement`1"/> within the given <paramref name="tolerance"/>; otherwise, <c>false</c>.
        /// </returns>
        public bool Equals(Measurement<TUnit> other, double tolerance, TUnit unit)
        {
            if (other == null) return false;
            return Math.Abs(this[unit] - other[unit]) <= tolerance;
        }        
        
        /// <summary>
        /// Serves as a hash function for a <see cref="Measurements.Measurement`1"/> object.
        /// </summary>
        /// <returns>
        /// A hash code for this instance that is suitable for use in hashing algorithms and data structures such as a
        /// hash table.
        /// </returns>
        public override int GetHashCode()
        {
            return BaseUnitValue.GetHashCode();
        }
        
        /// <summary>
        /// Determines whether a specified instance of <see cref="Measurement<TUnit>"/> is equal to another specified <see cref="Measurement<TUnit>"/>.
        /// </summary>
        /// <param name='t1'>
        /// The first <see cref="Measurement<TUnit>"/> to compare.
        /// </param>
        /// <param name='t2'>
        /// The second <see cref="Measurement<TUnit>"/> to compare.
        /// </param>
        /// <returns>
        /// <c>true</c> if <c>t1</c> and <c>t2</c> are equal; otherwise, <c>false</c>.
        /// </returns>
        public static bool operator ==(Measurement<TUnit> t1, Measurement<TUnit> t2)
        {
            if (Object.ReferenceEquals(t1, t2)) return true;
            if (((object)t1 == null) || ((object)t2 == null)) return false;
            return t1.BaseUnitValue == t2.BaseUnitValue;
        }
  
        /// <summary>
        /// Determines whether a specified instance of <see cref="Measurement<TUnit>"/> is not equal to another
        /// specified <see cref="Measurement<TUnit>"/>.
        /// </summary>
        /// <param name='t1'>
        /// The first <see cref="Measurement<TUnit>"/> to compare.
        /// </param>
        /// <param name='t2'>
        /// The second <see cref="Measurement<TUnit>"/> to compare.
        /// </param>
        /// <returns>
        /// <c>true</c> if <c>t1</c> and <c>t2</c> are not equal; otherwise, <c>false</c>.
        /// </returns>
        public static bool operator !=(Measurement<TUnit> t1, Measurement<TUnit> t2)
        {
            if (ReferenceEquals(t1, t2)) return false;
            if (((object)t1 == null) || ((object)t2 == null)) return true;
            return t1.BaseUnitValue != t2.BaseUnitValue;
        }
  
        /// <summary>
        /// Determines whether one specified <see cref="Measurement<TUnit>"/> is greater than another specfied <see cref="Measurement<TUnit>"/>.
        /// </summary>
        /// <param name='t1'>
        /// The first <see cref="Measurement<TUnit>"/> to compare.
        /// </param>
        /// <param name='t2'>
        /// The second <see cref="Measurement<TUnit>"/> to compare.
        /// </param>
        /// <returns>
        /// <c>true</c> if <c>t1</c> is greater than <c>t2</c>; otherwise, <c>false</c>.
        /// </returns>
        /// <exception cref='ArgumentNullException'>
        /// Thrown if either argument is null.
        /// </exception>
        public static bool operator >(Measurement<TUnit> t1, Measurement<TUnit> t2)
        {
            if (t1 == null) throw new ArgumentNullException("t1");
            if (t2 == null) throw new ArgumentNullException("t2");
            return t1.BaseUnitValue > t2.BaseUnitValue;
        }
  
        /// <summary>
        /// Determines whether one specified <see cref="Measurement<TUnit>"/> is lower than another specfied <see cref="Measurement<TUnit>"/>.
        /// </summary>
        /// <param name='t1'>
        /// The first <see cref="Measurement<TUnit>"/> to compare.
        /// </param>
        /// <param name='t2'>
        /// The second <see cref="Measurement<TUnit>"/> to compare.
        /// </param>
        /// <returns>
        /// <c>true</c> if <c>t1</c> is lower than <c>t2</c>; otherwise, <c>false</c>.
        /// </returns>
        /// <exception cref='ArgumentNullException'>
        /// Thrown if either argument is null.
        /// </exception>
        public static bool operator <(Measurement<TUnit> t1, Measurement<TUnit> t2)
        {
            if (t1 == null) throw new ArgumentNullException("t1");
            if (t2 == null) throw new ArgumentNullException("t2");
            return t1.BaseUnitValue < t2.BaseUnitValue;
        }
  
        /// <summary>
        /// Determines whether one specified <see cref="Measurement<TUnit>"/> is greater than or equal to another
        /// specfied <see cref="Measurement<TUnit>"/>.
        /// </summary>
        /// <param name='t1'>
        /// The first <see cref="Measurement<TUnit>"/> to compare.
        /// </param>
        /// <param name='t2'>
        /// The second <see cref="Measurement<TUnit>"/> to compare.
        /// </param>
        /// <returns>
        /// <c>true</c> if <c>t1</c> is greater than or equal to <c>t2</c>; otherwise, <c>false</c>.
        /// </returns>
        /// <exception cref='ArgumentNullException'>
        /// Thrown if either argument is null.
        /// </exception>
        public static bool operator >=(Measurement<TUnit> t1, Measurement<TUnit> t2)
        {
            if (t1 == null) throw new ArgumentNullException("t1");
            if (t2 == null) throw new ArgumentNullException("t2");
            return t1.BaseUnitValue >= t2.BaseUnitValue;
        }
  
        /// <summary>
        /// Determines whether one specified <see cref="Measurement<TUnit>"/> is lower than or equal to another specfied <see cref="Measurement<TUnit>"/>.
        /// </summary>
        /// <param name='t1'>
        /// The first <see cref="Measurement<TUnit>"/> to compare.
        /// </param>
        /// <param name='t2'>
        /// The second <see cref="Measurement<TUnit>"/> to compare.
        /// </param>
        /// <returns>
        /// <c>true</c> if <c>t1</c> is lower than or equal to <c>t2</c>; otherwise, <c>false</c>.
        /// </returns>
        /// <exception cref='ArgumentNullException'>
        /// Thrown if either argument is null.
        /// </exception>
        public static bool operator <=(Measurement<TUnit> t1, Measurement<TUnit> t2)
        {
            if (t1 == null) throw new ArgumentNullException("t1");
            if (t2 == null) throw new ArgumentNullException("t2");
            return t1.BaseUnitValue <= t2.BaseUnitValue;
        }        
        
        /// <summary>
        /// Creates an object used to convert the specified <paramref name="measurement"/>
        /// into another unit.
        /// </summary>
        /// <param name='measurement'>
        /// The measurement to be converted.
        /// </param>
        public static MeasurementUnitConvertible<TUnit> Convert(double measurement)
        {
            return new MeasurementUnitConvertible<TUnit>(measurement);   
        }
  
        /// <summary>
        /// Creates a string of the measurement using the specified <paramref name="unit"/>.
        /// </summary>
        /// <returns>
        /// A string representation of the measurement.
        /// </returns>
        /// <param name='format'>
        /// The numerical format that should be used for the string.
        /// </param>
        /// <param name='unit'>
        /// The unit of measurement to be used for the string.
        /// </param>
        public string ToString(string format, TUnit unit)
        {
            return ToString(format, unit, null);   
        }

        /// <summary>
        /// Creates a string of the measurement using the specified <paramref name="unit"/>.
        /// </summary>
        /// <returns>
        /// A string representation of the measurement.
        /// </returns>
        /// <param name='format'>
        /// The numerical format that should be used for the string.
        /// </param>
        /// <param name='unit'>
        /// The unit of measurement to be used for the string.
        /// </param>
        /// <param name='formatProvider'>
        /// The format provider used to format the measurement value.
        /// </param>
        public string ToString(string format, TUnit unit, IFormatProvider formatProvider)
        {
            if (unit == null) throw new ArgumentNullException("unit");
            if (formatProvider == null) formatProvider = CultureInfo.CurrentCulture;
            return this[unit].ToString(format, formatProvider) + " " + unit.Unit;
        }
    }
    
    /// <summary>
    /// A measurement object that can be created and cloned.
    /// </summary>
    /// <typeparam name="TUnit">
    /// The unit that this value measures.
    /// </typeparam>
    /// <typeparam name="TMeasurement">
    /// The type of measurement that can be created and cloned.
    /// </typeparam>
    public class Measurement<TMeasurement, TUnit> : Measurement<TUnit>, ICloneable
        where TUnit : MeasurementUnit
        where TMeasurement : Measurement<TMeasurement, TUnit>, new()
    {   
        private static IEnumerable<TUnit> GetUnits()
        {             
            var fields = typeof(TMeasurement).GetFields(
                BindingFlags.Static | BindingFlags.Public
            );
            
            var units = new List<TUnit>();
            foreach (var field in fields)
            {
                var baseType = field.FieldType;
                while (null != baseType)
                {
                    if (baseType.Equals(typeof(TUnit)))
                    {
                        units.Add(field.GetValue(null) as TUnit);   
                        break;
                    }
                    baseType = baseType.BaseType;
                }
            }
            return units;
        }
        
        public static readonly IEnumerable<TUnit> Units = GetUnits();
        
        /// <summary>
        /// Create the specified measurement.
        /// </summary>
        /// <param name='measurement'>
        /// The initial base-unit value of the measurement.
        /// </param>
        public static TMeasurement Create(double measurement)
        {
            var obj = new TMeasurement();
            obj.BaseUnitValue = measurement;
            return obj;
        }
        
        /// <summary>
        /// Create the specified measurement using the specified unit.
        /// </summary>
        /// <param name='measurement'>
        /// The initial value of the measurement in the specified <paramref name="unit"/>.
        /// </param>
        /// <param name='unit'>
        /// The unit that defines how the specified <paramref name="measurement"/>
        /// defines the created value.
        /// </param>
        public static TMeasurement Create(double measurement, TUnit unit)
        {
            var obj = new TMeasurement();
            obj[unit] = measurement;
            return obj;
        }      
  
        /// <summary>
        /// Creates a new measurement using the value of this instance.
        /// </summary>
        public object Clone()
        {
            var obj = new TMeasurement();
            obj.BaseUnitValue = BaseUnitValue;
            return obj;
        }
    }
}
